﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NeuralNetworkPro
{
    [Serializable]
    public class BackPropagation
    {

        private double[][][] delta_w_old;
        private NeuralNetwork network;

        public NeuralNetwork Network
        {
            get { return network; }
            private set { network = value; }
        }

        private double learningRate;

        public double LearningRate
        {
            get { return learningRate; }
            set { learningRate = value; }
        }

        private double momentumTerm;

        public double MomentumTerm
        {
            get { return momentumTerm; }
            set { momentumTerm = value; }
        }

        private double lastError;

        public double LastError
        {
            get { return lastError; }
        }

        private double lastEpochError;

        public double LastEpochError
        {
            get { return lastEpochError; }
        }        

        public BackPropagation(NeuralNetwork net, double learning_rate, double momentum_term)
        {
            this.network = net;
            this.learningRate = learning_rate;
            this.momentumTerm = momentum_term;
            delta_w_old = new double[net.LayersCount][][];
            delta_w_old[0] = new double[1][];
            delta_w_old[0][0] = new double[1];
            for (int m = 1; m < net.LayersCount; m++)
            {
                var layer = net[m];
                delta_w_old[m] = new double[layer.NeuronsCount][];
                for (int i = 0; i < layer.NeuronsCount; i++)
                {
                    var neuron = layer[i];
                    delta_w_old[m][i] = new double[neuron.InputsCount];
                }
            }

        }

        public double Run(double[] input, double[] output)
        {
            double error = 0.0;
            var o = network.Compute(input);

            UpdateWeight(output, o);

            for (int i = 0; i < output.Length; i++)
            {
                error += Math.Pow(o[i] - output[i], 2);
            }
            lastError = error / 2.0;
            return error / 2.0;
        }

        private void UpdateWeight(double[] y, double[] o)
        {
            var output_layer = network.LayersCount - 1;
            double[][] delta = new double[network.LayersCount][];

            delta[output_layer] = new double[network[output_layer].NeuronsCount];
            for (int i = 0; i < network[output_layer].NeuronsCount; i++)
            {
                double h = network[output_layer][i].InputSum;
                delta[output_layer][i] = network[output_layer][i].ActivationFunction.Derivative(h) * (y[i] - o[i]);
            }

            for (int m = network.LayersCount - 2; m >= 1; m--)
            {
                delta[m] = new double[network[m].NeuronsCount];
                for (int i = 0; i < network[m].NeuronsCount; i++)
                {
                    double h = network[m][i].InputSum;
                    double sum_front = 0;
                    for (int j = 0; j < network[m + 1].NeuronsCount; j++)
                    {
                        sum_front += network[m + 1][j][i] * delta[m + 1][j];
                    }
                    delta[m][i] = network[m][i].ActivationFunction.Derivative(h) * sum_front;
                }
            }


            
            for (int m = 1; m < network.LayersCount; m++)
            {
                //System.Threading.Tasks.Parallel.For(0, network[m].NeuronsCount, i =>
                for (int i = 0; i < network[m].NeuronsCount; i++)
                {
                    for (int j = 0; j < network[m][i].InputsCount; j++)
                    {
                        var d_w_old = delta_w_old[m][i][j];

//                        var w_new = learningRate * ((1 - momentumTerm) * delta[m][i] * network[m - 1][j].Output + (momentumTerm * d_w_old));
                        var j_out = j == network[m -1].NeuronsCount ? -1 : network[m - 1][j].Output;
                        var w_new = (learningRate * delta[m][i] * j_out) + (momentumTerm * d_w_old);
                        delta_w_old[m][i][j] = w_new;
                        network[m][i][j] += w_new;
                    }
                }//);
            }
        }

        public double RunEpoch(double[][] input, double[][] output)
        {
            double error = 0.0;

            // run learning procedure for all samples
            for (int i = 0, n = input.Length; i < n; i++)
            {
                error += Run(input[i], output[i]);
            }

            lastEpochError = error;
            // return summary error
            return error;
        }

        private BackPropagation()
        {}

        #region Save and load functions
        public void BinarySave(string name)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            var stream = new System.IO.StreamWriter(name + ".sav");
            stream.BaseStream.Position = 0L;
            formatter.Serialize(stream.BaseStream, this);
            stream.Close();
            stream.Dispose();
        }

        public static BackPropagation BinaryLoad(string name)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            var stream = new System.IO.StreamReader(name + ".sav");
            stream.BaseStream.Position = 0L;
            var res = formatter.Deserialize(stream.BaseStream) as BackPropagation;
            stream.Close();
            stream.Dispose();
            return res;
        }

        internal void ToStream(StreamWriter stream)
        {
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
            formatter.Serialize(stream.BaseStream, (new double[] {learningRate, momentumTerm, lastEpochError}));
            formatter.Serialize(stream.BaseStream, delta_w_old);
            network.ToStream(stream);
            //return res;
        }

        internal static BackPropagation FromStream(StreamReader stream)
        {
            BackPropagation res = new BackPropagation();
            var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();

            var learnng_mom = formatter.Deserialize(stream.BaseStream)as double[];
            res.learningRate = learnng_mom[0];
            res.momentumTerm = learnng_mom[1];
            res.lastEpochError = learnng_mom[2];
            res.delta_w_old = formatter.Deserialize(stream.BaseStream) as double[][][];
            var network = NeuralNetwork.FromStream(stream);
            res.network = network;

            return res;
        }
        public void Save(string name)
        {
            var stream = new StreamWriter(name + ".sav");
            this.ToStream(stream);
            stream.Flush();
            stream.Close();
            //System.IO.File.WriteAllLines(name + ".sav", strs.ToArray());
        }
        public static BackPropagation Load(string name)
        {
            return FromStream(new StreamReader(name + ".sav"));
        }
        #endregion
    }
}
